//
//  HHPRequestCache.m
//  HHPLiveVideo
//
//  Created by 韩红坡 on 2016/12/15.
//  Copyright © 2016年 韩红坡. All rights reserved.
//

#import "HHPRequestCache.h"

#import <CommonCrypto/CommonDigest.h>

#define kAFURLCachePath @"HHPNetworkingURLCache"
#define kAFURLCacheMaintenanceTime 5ull

#if !__has_feature(objc_arc)
#error "HHPRequestCache needs to be compiled with ARC enabled."
#endif


static NSTimeInterval const kAFURLCacheInfoDefaultMinCacheInterval = 5.0 * 60.0; // 5 minute
static NSString *const kAFURLCacheInfoFileName = @"cacheInfo.plist";
static NSString *const kAFURLCacheInfoAccessesKey = @"accesses";
static NSString *const kAFURLCacheInfoSizesKey = @"sizes";
static NSString *const kAFURLCacheInfoURLsKey = @"URLs";

@interface HHPRequestCache () {
    BOOL _timerPaused;
    NSUInteger _diskCacheUsage;
    BOOL _diskCacheInfoDirty;
}

/**
 *   The default value is set to 5 minutes (300 seconds).
 */
@property (nonatomic, assign) NSTimeInterval minCacheInterval;


@property (nonatomic, copy) NSString *diskCachePath;

@property (nonatomic, strong) dispatch_source_t maintenanceTimer;

@property (nonatomic, retain) NSMutableDictionary *diskCacheInfo;

@end

@implementation HHPRequestCache

+ (HHPRequestCache *)sharedRequestCache
{
    
    static HHPRequestCache *requestCache = nil;
    static dispatch_once_t oncePredicate;
    
    dispatch_once(&oncePredicate, ^{
        requestCache = [[HHPRequestCache alloc] initWithMemoryCapacity:5 * 1024 * 1024 diskCapacity:40 * 1024 * 1024 diskPath:[HHPRequestCache defaultCachePath]];
    });
    
    return requestCache;
}

#pragma mark HHPURLCache (tools)

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    NSString *string = request.URL.absoluteString;
    NSRange hash = [string rangeOfString:@"#"];
    if (hash.location == NSNotFound)
        return request;
    
    NSMutableURLRequest *copy = [request mutableCopy];
    copy.URL = [NSURL URLWithString:[string substringToIndex:hash.location]];
    return copy;
}

+ (NSString *)cacheKeyForURL:(NSURL *)url
{
    const char *str = [url.absoluteString UTF8String];
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    static NSString *cacheFormatVersion = @"2";
    return [NSString stringWithFormat:@"%@_%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
            cacheFormatVersion, r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15]];
}

#pragma mark SDURLCache (private)

static dispatch_queue_t get_disk_cache_queue()
{
    static dispatch_once_t onceToken;
    static dispatch_queue_t _diskCacheQueue;
    dispatch_once(&onceToken, ^{
        _diskCacheQueue = dispatch_queue_create("com.petersteinberger.disk-cache.processing", NULL);
    });
    return _diskCacheQueue;
}

static dispatch_queue_t get_disk_io_queue()
{
    static dispatch_queue_t _diskIOQueue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _diskIOQueue = dispatch_queue_create("com.petersteinberger.disk-cache.io", NULL);
    });
    return _diskIOQueue;
}

- (dispatch_source_t)maintenanceTimer
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        _maintenanceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        if (_maintenanceTimer) {
            dispatch_source_set_timer(_maintenanceTimer, dispatch_walltime(DISPATCH_TIME_NOW, kAFURLCacheMaintenanceTime * NSEC_PER_SEC),
                                      kAFURLCacheMaintenanceTime * NSEC_PER_SEC, kAFURLCacheMaintenanceTime/2 * NSEC_PER_SEC);
            __block HHPRequestCache *blockSelf = self;
            dispatch_source_set_event_handler(_maintenanceTimer, ^{
                [blockSelf periodicMaintenance];
                
                // will abuse cache queue to lock timer
                dispatch_async(get_disk_cache_queue(), ^{
                    dispatch_suspend(_maintenanceTimer); // pause timer
                    _timerPaused = YES;
                });
            });
            // initially wake up timer
            dispatch_resume(_maintenanceTimer);
        }
    });
    return _maintenanceTimer;
}

#pragma mark HHPRequestCache

+ (NSString *)defaultCachePath
{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *resultPath = [[paths objectAtIndex:0] stringByAppendingPathComponent:kAFURLCachePath];
    return resultPath;
}

- (void)createDiskCachePath
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSFileManager *fileManager = [[NSFileManager alloc] init];
        if (![fileManager fileExistsAtPath:_diskCachePath]) {
            [fileManager createDirectoryAtPath:_diskCachePath
                   withIntermediateDirectories:YES
                                    attributes:nil
                                         error:NULL];
            //close iCloud data sync
            [self addSkipBackupAttributeToItemAtURL:[NSURL fileURLWithPath:_diskCachePath]];
        }
        
    });
}
- (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL
{
    assert([[NSFileManager defaultManager] fileExistsAtPath: [URL path]]);
    NSError *error = nil;
    BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES]
                                  forKey: NSURLIsExcludedFromBackupKey error: &error];
    if(!success){
        HHPLog(@"Error excluding %@ from backup %@", [URL lastPathComponent], error);
    }
    return success;
}

- (void)saveCacheInfo
{
    [self createDiskCachePath];
    dispatch_async(get_disk_cache_queue(), ^{
        // Previous versions of SDURLCache stored a diskUsage key that could go wrong, just get rid of it.
        [self.diskCacheInfo removeObjectForKey:@"diskUsage"];
        //NSData *data = [NSPropertyListSerialization dataFromPropertyList:self.diskCacheInfo format:NSPropertyListBinaryFormat_v1_0 errorDescription:NULL];
        NSData *data = [NSPropertyListSerialization dataWithPropertyList:self.diskCacheInfo format:NSPropertyListBinaryFormat_v1_0 options:0 error:NULL];
        if (data) {
            [data writeToFile:[_diskCachePath stringByAppendingPathComponent:kAFURLCacheInfoFileName] atomically:YES];
        }
        
        _diskCacheInfoDirty = NO;
    });
}

- (void)removeCachedResponseForCachedKeys:(NSArray *)cacheKeys
{
    dispatch_async(get_disk_cache_queue(), ^{
        @autoreleasepool {
            NSEnumerator *enumerator = [cacheKeys objectEnumerator];
            NSString *cacheKey;
            
            NSMutableDictionary *accesses = [self.diskCacheInfo objectForKey:kAFURLCacheInfoAccessesKey];
            NSMutableDictionary *sizes = [self.diskCacheInfo objectForKey:kAFURLCacheInfoSizesKey];
#if SDURLCACHE_DEBUG
            NSMutableDictionary *urls = [self.diskCacheInfo objectForKey:kAFURLCacheInfoURLsKey];
#endif
            NSFileManager *fileManager = [[NSFileManager alloc] init];
            
            while ((cacheKey = [enumerator nextObject])) {
                NSUInteger cacheItemSize = [[sizes objectForKey:cacheKey] unsignedIntegerValue];
                [accesses removeObjectForKey:cacheKey];
                [sizes removeObjectForKey:cacheKey];
#if SDURLCACHE_DEBUG
                [urls removeObjectForKey:cacheKey];
#endif
                [fileManager removeItemAtPath:[_diskCachePath stringByAppendingPathComponent:cacheKey] error:NULL];
                
                _diskCacheUsage -= cacheItemSize;
            }
        }
    });
}


- (void)balanceDiskUsage
{
    if (_diskCacheUsage < self.diskCapacity) {
        return; // Already done
    }
    
    dispatch_async(get_disk_cache_queue(), ^{
        NSMutableArray *keysToRemove = [NSMutableArray array];
        
        // Apply LRU cache eviction algorithm while disk usage outreach capacity
        NSDictionary *sizes = [self.diskCacheInfo objectForKey:kAFURLCacheInfoSizesKey];
        
        NSInteger capacityToSave = _diskCacheUsage - self.diskCapacity;
        NSArray *sortedKeys = [[self.diskCacheInfo objectForKey:kAFURLCacheInfoAccessesKey] keysSortedByValueUsingSelector:@selector(compare:)];
        NSEnumerator *enumerator = [sortedKeys objectEnumerator];
        NSString *cacheKey;
        
        while (capacityToSave > 0 && (cacheKey = [enumerator nextObject])) {
            [keysToRemove addObject:cacheKey];
            capacityToSave -= [(NSNumber *)[sizes objectForKey:cacheKey] unsignedIntegerValue];
        }
        
        [self removeCachedResponseForCachedKeys:keysToRemove];
        [self saveCacheInfo];
    });
}

- (NSMutableDictionary *)diskCacheInfo
{
    if (!_diskCacheInfo) {
        dispatch_sync(get_disk_cache_queue(), ^{
            if (!_diskCacheInfo) { // Check again, maybe another thread created it while waiting for the mutex
                _diskCacheInfo = [[NSMutableDictionary alloc] initWithContentsOfFile:[_diskCachePath stringByAppendingPathComponent:kAFURLCacheInfoFileName]];
                if (!_diskCacheInfo) {
                    _diskCacheInfo = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
                                      [NSMutableDictionary dictionary], kAFURLCacheInfoAccessesKey,
                                      [NSMutableDictionary dictionary], kAFURLCacheInfoSizesKey,
#if SDURLCACHE_DEBUG
                                      [NSMutableDictionary dictionary], kAFURLCacheInfoURLsKey,
#endif
                                      nil];
                }
                _diskCacheInfoDirty = NO;
                NSArray *sizes = [[_diskCacheInfo objectForKey:kAFURLCacheInfoSizesKey] allValues];
                _diskCacheUsage = [[sizes valueForKeyPath:@"@sum.self"] unsignedIntegerValue];
                
                // create maintenance timer
                [self maintenanceTimer];
            }
        });
    }
    
    return _diskCacheInfo;
}

- (void)storeRequestToDisk:(NSURLRequest *)request response:(id)cachedResponse
{
    
    if (!request ||
        !cachedResponse) {
        return;
    }
    
    NSString *cacheKey = [[self class] cacheKeyForURL:request.URL];
    NSString *cacheFilePath = [_diskCachePath stringByAppendingPathComponent:cacheKey];
    
    [self createDiskCachePath];
    
    // Archive the cached response on disk
    if (![NSKeyedArchiver archiveRootObject:cachedResponse toFile:cacheFilePath]) {
        // Caching failed for some reason
        return;
    }
    
    // Update disk usage info
    NSFileManager *fileManager = [[NSFileManager alloc] init];
    NSNumber *cacheItemSize = [[fileManager attributesOfItemAtPath:cacheFilePath error:NULL] objectForKey:NSFileSize];
    
    dispatch_async(get_disk_cache_queue(), ^{
        
        NSNumber *previousCacheItemSize = [[self.diskCacheInfo objectForKey:kAFURLCacheInfoSizesKey] objectForKey:cacheKey];
        _diskCacheUsage -= [previousCacheItemSize unsignedIntegerValue];
        _diskCacheUsage += [cacheItemSize unsignedIntegerValue];
        
        // Update cache info for the stored item
        [(NSMutableDictionary *)[self.diskCacheInfo objectForKey:kAFURLCacheInfoAccessesKey] setObject:[NSDate date] forKey:cacheKey];
        [(NSMutableDictionary *)[self.diskCacheInfo objectForKey:kAFURLCacheInfoSizesKey] setObject:cacheItemSize forKey:cacheKey];
#if SDURLCACHE_DEBUG
        [(NSMutableDictionary *)[self.diskCacheInfo objectForKey:kAFURLCacheInfoURLsKey] setObject:request.URL.absoluteString forKey:cacheKey];
#endif
        
        [self saveCacheInfo];
        
        // start timer for cleanup (rely on fact that dispatch_suspend syncs with disk cache queue)
        if (_timerPaused) {
            _timerPaused = NO;
            dispatch_resume([self maintenanceTimer]);
        }
    });
}

// called in NSTimer
- (void)periodicMaintenance
{
    if (_diskCacheUsage > self.diskCapacity) {
        dispatch_async(get_disk_io_queue(), ^{
            [self balanceDiskUsage];
        });
    }
    else if (_diskCacheInfoDirty) {
        dispatch_async(get_disk_io_queue(), ^{
            [self saveCacheInfo];
        });
    }
}

#pragma mark NSURLCache

- (instancetype)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(NSString *)path
{
    self = [super initWithMemoryCapacity:memoryCapacity diskCapacity:diskCapacity diskPath:path];
    if (self) {
        
        self.minCacheInterval = kAFURLCacheInfoDefaultMinCacheInterval;
        self.diskCachePath = path;
        
    }
    return self;
}

- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request
{
    request = [[self class] canonicalRequestForRequest:request];
    
    if ((request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData
         || request.cachePolicy == NSURLRequestReloadIgnoringLocalAndRemoteCacheData
         || request.cachePolicy == NSURLRequestReloadIgnoringCacheData)) {
        // When cache is ignored for read, it's a good idea not to store the result as well as this option
        // have big chance to be used every times in the future for the same request.
        // NOTE: This is a change regarding default URLCache behavior
        return;
    }
    
    [super storeCachedResponse:cachedResponse forRequest:request];
    
    /*
     NSURLCacheStoragePolicy storagePolicy = cachedResponse.storagePolicy;
     if ((storagePolicy == NSURLCacheStorageAllowed || (storagePolicy == NSURLCacheStorageAllowedInMemoryOnly))
     && [cachedResponse.response isKindOfClass:[NSHTTPURLResponse self]]
     && cachedResponse.data.length < self.diskCapacity)
     {
     dispatch_async(get_disk_io_queue(), ^{
     [self storeRequestToDisk:request response:cachedResponse];
     });
     }*/
    
}

- (void)creatDiskCacheForURLRequest:(NSURLRequest *)urlRequest responseObj:(id)responsObj
{
    
    /*
     dispatch_async(get_disk_io_queue(), ^{
     [self storeRequestToDisk:urlRequest response:responsObj];
     });*/
    [self storeRequestToDisk:urlRequest response:responsObj];
    
}

#pragma mark - public API
- (BOOL)isCached:(NSURL *)url
{
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    request = [[self class] canonicalRequestForRequest:request];
    
    NSString *cacheKey = [[self class] cacheKeyForURL:url];
    NSString *cacheFile = [_diskCachePath stringByAppendingPathComponent:cacheKey];
    
    BOOL isCached = [[[NSFileManager alloc] init] fileExistsAtPath:cacheFile];
    return isCached;
}

- (void)removeAllCachedResponsesInMemory
{
    [super removeAllCachedResponses];
}

- (id)diskCacheForURLRequest:(NSURLRequest *)urlRequest
{
    
    NSURL *url = urlRequest.URL;
    
    id cacheResponse = nil;
    
    NSString *cacheKey = [[self class] cacheKeyForURL:url];
    NSString *cacheFile = [_diskCachePath stringByAppendingPathComponent:cacheKey];
    
    if (cacheFile.length) {
        cacheResponse = [NSKeyedUnarchiver unarchiveObjectWithFile:cacheFile];
    }
    
    return cacheResponse;
}

@end
